package gateway.config.security.auth;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Sets;
import common.config.exception.CustomException;
import common.config.http.HttpStatusEnum;
import common.config.jwt.JwtUtil;
import common.model.dto.LoginToken;
import common.model.dto.UserCacheInfo;
import common.model.enums.EnvironmentEnum;
import common.model.enums.UserValidEnum;
import common.model.response.base.Result;
import common.service.RedissonCacheService;
import common.util.*;
import gateway.model.converter.SecurityConverter;
import gateway.model.dto.SecurityUser;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
import org.redisson.api.RMap;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * 顺序：1
 * 验证用户的登录状态，验证通过则同时注入角色权限信息和登录用户基本信息，否则会被security当作未登录
 *
 * @author 米泽鹏
 */
@Slf4j
@Component
public class CustomOncePerRequestFilter extends OncePerRequestFilter {

	private static final String UPLOAD = "upload";
	private static final String DOWNLOAD = "download";

	@Resource
	private CustomAuthenticationExceptionHandler customAuthenticationExceptionHandler;
	@Resource
	private RedissonCacheService redissonCacheService;
	@Resource
	private Environment environment;

	@Override
	protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
		MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
		MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);
		StopWatch stopWatch = new StopWatch();
		try {
			stopWatch.start();
			log.info("请求url：" + request.getRequestURI());
			log.info("请求头类型：" + request.getContentType());
			// 获取token设置全局User信息
			if (!PublicUrlAntPatternUtil.isPublicUrl(request)) {
				setSecurityUserContext(request);
			}
			// TODO 如何判断是上传请求
			// 处理上传 FormData 不能读取
			if (request.getRequestURI().contains(UPLOAD)) {
				filterChain.doFilter(request, response);
				return;
			}
			// 记录请求的消息体
			logRequestBody(wrappedRequest);
			// TODO 如何判断是下载请求
			if (isIllegalContentType(request) || request.getRequestURI().contains(DOWNLOAD)) {
				filterChain.doFilter(wrappedRequest, response);
				return;
			}
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		} catch (JwtException e) {
			if (SecurityConstants.LOGOUT_PROCESSING_URL.equals(request.getRequestURI())) {
				filterChain.doFilter(wrappedRequest, response);
				return;
			}
			log.error(e.getMessage(), e);
			SecurityContextHolder.clearContext();
			ResponseUtil.out(response, Result.error(HttpStatusEnum.UNAUTHORIZED.getCode(), e instanceof ExpiredJwtException ? "登录超时，请重新登录" : "请重新登录：" + ExceptionUtil.getSimpleMessage(e)));
		} catch (AuthenticationException e) {
			log.error("鉴权失败", e);
			SecurityContextHolder.clearContext();
			customAuthenticationExceptionHandler.commence(request, response, e);
		} catch (CustomException e) {
			log.error("出现其他异常", e);
			ResponseUtil.out(response, Result.error(e.getCode() == null ? HttpStatusEnum.INTERNAL_SERVER_ERROR.getCode() : e.getCode(), ExceptionUtil.getSimpleMessage(e)));
		} catch (Exception e) {
			log.error("出现其他异常", e);
			ResponseUtil.out(response, Result.error(HttpStatusEnum.INTERNAL_SERVER_ERROR.getCode(), ExceptionUtil.getSimpleMessage(e)));
		} finally {
			stopWatch.stop();
			long usedTimes = stopWatch.getTotalTimeMillis();
			// 记录响应的消息体
			logResponseBody(request, wrappedResponse, usedTimes);
		}
	}

	private void setSecurityUserContext(HttpServletRequest request) {
		String token = JwtUtil.getTokenStringFromRequest(request);
		if (StrUtil.isNotBlank(token)) {
			LoginToken loginToken = JwtUtil.parseLoginToken(token);
			if (NullChecker.checkAnyNull(loginToken, loginToken.getUserId(), loginToken.getUsername(), loginToken.getNickname(), loginToken.getRoleIdSet(), loginToken.getAuthorityIdSet())) {
				throw new BadCredentialsException("用户信息不完整，请重新登录");
			}
			if (!SecurityConstants.LOGOUT_PROCESSING_URL.equals(request.getRequestURI())) {
				// 检查token是否是最新的
				String tokenInfoCache = redissonCacheService.getLoginTokenStringByUserId(loginToken.getUserId());
				if (tokenInfoCache == null) {
					throw new CredentialsExpiredException("未登录或登录超时，请重新登录");
				}
				assert token != null;
				if (!Constants.INNER_VISITOR_USERNAME.equals(loginToken.getUsername()) && !token.equals(tokenInfoCache)) {
					throw new BadCredentialsException("当前账号已在其他位置登录，请重新登录");
				}
				// 检查账号基本信息是否已变更以及是否被禁用
				List<UserCacheInfo> userValidCache = redissonCacheService.getSysUserCache();
				UserCacheInfo userCacheInfo = userValidCache.stream().filter(item -> Objects.requireNonNull(item.getUserId()).equals(loginToken.getUserId())).findFirst().orElse(null);
				if (userCacheInfo == null) {
					log.error("缓存中未找到该用户的信息：{}", JSONUtil.toJsonStr(loginToken));
					throw new BadCredentialsException("无效用户");
				} else {
					if (!Objects.equals(loginToken.getUsername(), userCacheInfo.getUsername()) || !Objects.equals(loginToken.getNickname(), userCacheInfo.getNickname())) {
						throw new BadCredentialsException("账号信息已变更，请重新登录");
					}
					if (!UserValidEnum.VALID.getValue().equals(userCacheInfo.getValid())) {
						throw new DisabledException("账号被禁用");
					}
				}
				// 检查当前用户的权限信息是否已变更
				// 检查用户的角色变化
				RMap<Long, Set<Long>> userRoleCacheByRoleId = redissonCacheService.getAllRoleIdCacheByUserId();
				Set<Long> roleIdSetInCache = userRoleCacheByRoleId.get(loginToken.getUserId());
				if (!SetUtils.isEqualSet(roleIdSetInCache, loginToken.getRoleIdSet())) {
					throw new BadCredentialsException("权限信息已变更，请重新登录");
				}
				// 检查角色的权限变化
				RMap<Long, Set<Long>> roleAuthorityCacheByRoleId = redissonCacheService.getAllAuthIdCacheByRoleId();
				Set<Long> authIdSetInCache = Sets.newHashSet();
				for (Long roleId : loginToken.getRoleIdSet()) {
					Set<Long> authIdSetInCurrentRole = roleAuthorityCacheByRoleId.get(roleId);
					if (CollUtil.isNotEmpty(authIdSetInCurrentRole)) {
						authIdSetInCache.addAll(authIdSetInCurrentRole);
					}
				}
				if (!SetUtils.isEqualSet(authIdSetInCache, loginToken.getAuthorityIdSet())) {
					throw new BadCredentialsException("权限信息已变更，请重新登录");
				}
			}
			// 全局注入角色权限信息和登录用户基本信息
			SecurityUser securityUser = SecurityConverter.loginTokenToSecurityUser(loginToken);
			securityUser.setToken(token);
			UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
			SecurityContextHolder.getContext().setAuthentication(authentication);
		} else {
			throw new CredentialsExpiredException("无法获取用户信息，请重新登录");
		}
	}

	private boolean isIllegalContentType(HttpServletRequest request) {
		return (request.getContentType() == null && request.getContentLength() > 0) || (request.getContentType() != null && !request.getContentType().contains(Constants.HEADER_CONTENT_TYPE));
	}

	private void logRequestBody(MultiReadHttpServletRequest request) {
		if (request != null) {
			try {
				String bodyJson = request.getBodyJsonStrByJson(request);
				String url = request.getRequestURI().replace("//", "/");
				if (StrUtil.isBlank(bodyJson)) {
					log.info("请求：{}，接收到的body参数：无", url);
				} else {
					log.info("请求：{}，接收到的body参数：{}", url, JSONUtil.isTypeJSON(bodyJson) ? JSONUtil.toJsonStr(bodyJson) : bodyJson);
				}
			} catch (Exception e) {
				log.error(ExceptionUtil.getRootCauseMessage(e));
			}
		}
	}

	private void logResponseBody(HttpServletRequest request, MultiReadHttpServletResponse wrapper, long useTime) {
		if (wrapper != null) {
			byte[] buf = wrapper.getBody();
			if (buf.length > 0) {
				String payload;
				try {
					payload = new String(buf, wrapper.getCharacterEncoding());
				} catch (UnsupportedEncodingException ex) {
					payload = "[unknown]";
				}
				if (request != null) {
					if (environment.acceptsProfiles(Profiles.of(EnvironmentEnum.DEV.getName(), EnvironmentEnum.TEST.getName()))) {
						log.info("请求：{}，耗时：{}ms，返回的数据：{}", request.getRequestURI(), useTime, JSONUtil.isTypeJSON(payload) ? "\n" + JSONUtil.toJsonPrettyStr(payload) : payload);
					} else {
						log.info("请求：{}，耗时：{}ms", request.getRequestURI(), useTime);
					}
				}
			}
		}
	}

}
